/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.apache.ibatis.reflection;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

public class ParamNameResolver {

	private static final String GENERIC_NAME_PREFIX = "param";

	/**
	 * <p>
	 * The key is the index and the value is the name of the parameter.<br />
	 * The name is obtained from {@link Param} if specified. When {@link Param}
	 * is not specified, the parameter index is used. Note that this index could
	 * be different from the actual index when the method has special parameters
	 * (i.e. {@link RowBounds} or {@link ResultHandler}).
	 * </p>
	 * <ul>
	 * <li>aMethod(@Param("M") int a, @Param("N") int b) -&gt; {{0, "M"}, {1,
	 * "N"}}</li>
	 * <li>aMethod(int a, int b) -&gt; {{0, "0"}, {1, "1"}}</li>
	 * <li>aMethod(int a, RowBounds rb, int b) -&gt; {{0, "0"}, {2, "1"}}</li>
	 * </ul>
	 */
	private final SortedMap<Integer, String> names;

	private boolean hasParamAnnotation;

	/**对Mapper方法的所有参数信息进行遍历，首先判断参数中是否有@Param注解，如果包含@Param注解，就从注解中获取参数名称，如果参数中没有@Param注解，就根据Mybatis主配置文件中的useActualParamName
	 * <br/>参数确定是否获取实际方法定义的参数名称，若useActualParamName为true，则使用方法定义的参数名称。解析完毕后，将参数信息保存在一个不可修改的names属性中
	 * @param config
	 * @param method
	 */
	public ParamNameResolver(Configuration config, Method method) {
		// 获取参数类型
		final Class<?>[] paramTypes = method.getParameterTypes();
		// 获取所有参数注解
		final Annotation[][] paramAnnotations = method.getParameterAnnotations();
		final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
		int paramCount = paramAnnotations.length;
		// 从@Param注解中获取参数名称
		for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
			// 如果是RowBounds或者ResultHandler，则跳过
			if (isSpecialParameter(paramTypes[paramIndex])) {
				// skip special parameters
				continue;
			}
			String name = null;
			for (Annotation annotation : paramAnnotations[paramIndex]) {
				// 方法参数中是否有Param注解
				if (annotation instanceof Param) {
					hasParamAnnotation = true;
					// 获取参数名称
					name = ((Param) annotation).value();
					break;
				}
			}
			if (name == null) {
				// 未指定@Param注解，用于判断是否使用实际的参数名称，参考useActualParamName属性的作用
				if (config.isUseActualParamName()) {
					// 获取参数名
					name = getActualParamName(method, paramIndex);
				}
				if (name == null) {
					// use the parameter index as the name ("0", "1", ...) gcode issue #71
					name = String.valueOf(map.size());
				}
			}
			// 将参数信息保存在Map中，key为参数位置索引，value为参数名称
			map.put(paramIndex, name);
		}
		// 将参数信息保存在names属性中
		names = Collections.unmodifiableSortedMap(map);
	}

	private String getActualParamName(Method method, int paramIndex) {
		if (Jdk.parameterExists) {
			return ParamNameUtil.getParamNames(method).get(paramIndex);
		}
		return null;
	}

	private static boolean isSpecialParameter(Class<?> clazz) {
		return RowBounds.class.isAssignableFrom(clazz)
				|| ResultHandler.class.isAssignableFrom(clazz);
	}

	/**
	 * Returns parameter names referenced by SQL providers.
	 */
	public String[] getNames() {
		return names.values().toArray(new String[0]);
	}

	/**
	 * <p>
	 * A single non-special parameter is returned without a name.<br />
	 * Multiple parameters are named using the naming rule.<br />
	 * In addition to the default names, this method also adds the generic names
	 * (param1, param2, ...).
	 * </p>
	 */
	public Object getNamedParams(Object[] args) {
		final int paramCount = names.size();
		if (args == null || paramCount == 0) {
			return null;
		} else if (!hasParamAnnotation && paramCount == 1) {
			// 没有@Param注解，并且参数只有一个时，直接返回
			return args[names.firstKey()];
		} else {
			final Map<String, Object> param = new ParamMap<Object>();
			int i = 0;
			for (Map.Entry<Integer, String> entry : names.entrySet()) {
				param.put(entry.getValue(), args[entry.getKey()]);
				// add generic param names (param1, param2, ...)
				final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
				// ensure not to overwrite parameter named with @Param
				if (!names.containsValue(genericParamName)) {
					param.put(genericParamName, args[entry.getKey()]);
				}
				i++;
			}
			return param;
		}
	}
}
